import type {
    AxiosError,
    AxiosInstance,
    AxiosProgressEvent,
    AxiosRequestConfig,
    AxiosResponse,
    InternalAxiosRequestConfig
} from "axios";
import axios from "axios";
import { checkStatus } from "./checkStatus";
import { ElMessage } from "element-plus";

/** 上传文件的类型 */
export interface UploadFileParams<TOtherFormData = Record<string, any>> {
    // 文件其他的参数(对象 key-value 将作为表单数据)
    formData?: TOtherFormData;
    // 文件在表单中的字段名
    field?: string;
    // 文件数据对象
    file: File | Blob;
    // 文件名，也会作为表单数据之一
    filename?: string;
    // 文件hash，也会作为表单数据之一
    fileHash?: string;
    // [key: string]: any;
}

const DEFAULT_EXTRA_FEATURE_CONFIG = { showLoading: true, showMessage: true, debounce: true, retry: true };

/** 扩展 axios 的请求配置类型 */
export interface MyAxiosRequestConfig<TReqBodyData = any> extends AxiosRequestConfig<TReqBodyData> {
    interceptors?: {
        reqInterceptorOnFulfilled?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig;
        reqInterceptorOnRejected?: (err: AxiosError) => any;
        resInterceptorOnFulfilled?: (res: AxiosResponse) => AxiosResponse;
        resInterceptorOnRejected?: (err: AxiosError) => Promise<AxiosError>;
    };
    showLoading?: boolean;
    showMessage?: boolean;
    debounce?: boolean;
    retry?: boolean;
}

/** 给拦截器使用 */
export interface MyInternalAxiosRequestConfig extends InternalAxiosRequestConfig {
    showLoading?: boolean;
    showMessage?: boolean;
    debounce?: boolean;
    retry?: boolean;
}
export interface MyAxiosResponse extends AxiosResponse {
    config: MyInternalAxiosRequestConfig;
}
export interface MyAxiosError extends AxiosError {
    config: MyInternalAxiosRequestConfig;
}

/** 默认接口返回结构 */
export interface ResStructure<TResData = any> {
    code: number;
    data: TResData;
    msg?: string;
    // message?: string;
}

/**
 * A wrapper class for making HTTP requests using Axios.
 * @class HttpRequest
 * @example
 * // Usage example:
 * const httpRequest = new HttpRequest({baseURL: 'http://localhost:8888'});
 * httpRequest.get<TReqBodyData, TResData, TResStructure>({ url: '/users/1' })
 *   .then(response => {
 *     console.log(response.name); // logs the name of the user
 *   })
 *   .catch(error => {
 *     console.error(error);
 *   });
 *
 * @property {AxiosInstance} instance - The Axios instance used for making requests.
 */
class HttpRequest {
    private readonly instance: AxiosInstance;

    private readonly abortControllerMap: Map<string, AbortController>;

    private readonly extraConfig: Record<string, boolean> = DEFAULT_EXTRA_FEATURE_CONFIG;

    constructor(config: MyAxiosRequestConfig) {
        this.instance = axios.create(config);

        this.abortControllerMap = new Map();

        /* * 拦截器执行顺序，请求级请求拦截 -> 实例级 -> 全局级 -> 全局响应 -> 实例级 -> 请求级 */
        // 整个拦截器执行顺序是一个管道式(本质是数组)，从左往右，请求拦截到响应拦截。请求拦截从左边加入管道，响应拦截从右边加入管道。
        // 因此，请求拦截，最后加入管道的最先执行，响应拦截最先加入管道的最先执行。
        // 比如下面全局请求拦截中，执行顺序为 url加入map -> 添加token

        /* * 全局拦截 (所有 axios 实例都有的拦截器） */
        // 0. 为让在具体请求中设置的额外配置能在响应拦截器中获取，将请求 config 额外配置项注入到响应 config
        // 默认所有拦截器功能是打开的，只有具体请求中明确配置了 false 才是关闭，不写配置项也是默认也是开启
        this.instance.interceptors.request.use((config: any) => {
            Object.keys(this.extraConfig).forEach(item => {
                if (config[item] === false) this.extraConfig[item] = !!config[item];
            });
            return config;
        });
        this.instance.interceptors.response.use(
            (res: any) => {
                Object.keys(this.extraConfig).forEach(item => {
                    res.config[item] = this.extraConfig[item];
                    this.extraConfig[item] = true; // 配置复原成默认开启
                });
                return res;
            },
            (err: any) => {
                Object.keys(this.extraConfig).forEach(item => {
                    err.config[item] = this.extraConfig[item];
                    this.extraConfig[item] = true; // 配置复原成默认开启
                });
                throw err;
            }
        );
        // 1. 添加 token
        // this.instance.interceptors.request.use(config => {
        // const token = getToken()
        // if (token) config.headers.Authorization = `Bearer ${token}`
        //     return config;
        // });

        // 2. axios http 错误处理（超出 2xx 范围的 http 状态码都会触发该函数）
        this.instance.interceptors.response.use(null, (error: AxiosError) => {
            // 手动取消请求导致的错误触发，直接 return 略过
            if (error.message === "canceled") ElMessage.warning("请求取消成功");

            console.log("全局：错误处理提示", error);
            const { response } = error;
            // 1. 请求超时 && 网络错误单独判断，因为没有 response
            if (error.message.indexOf("timeout") !== -1) ElMessage.error("请求超时！请您稍后重试");
            if (error.message.indexOf("Network Error") !== -1) ElMessage.error("网络错误！请您稍后重试");
            // 2. 根据 http 服务器响应的错误状态码，做不同的处理
            if (response) {
                checkStatus(response.status); // elmessage 提示错误
                // switch (response.status) {
                //     case 404:
                //         console.log("你所访问的资源不存在！");
                //         break;
                //     case 500:
                //         console.log("服务异常！");
                //         break;
                //     default:
                //         console.log("请求失败！");
                // }
            }
            // 3. 服务器结果都没有返回(可能服务器错误可能客户端断网)，断网处理:也可以跳转到断网页面
            if (!window.navigator.onLine) ElMessage.error("服务器错误或者无网络"); // router.replace("/500");
            throw error;
        });

        // 3. 取消请求
        // 为每个请求都生成一个 signal，并以 url 为 key 添加入 map
        this.instance.interceptors.request.use(
            config => {
                // 如果具体方法中设置了 signal，这里就不再添加，避免覆盖。
                // 具体场景：文件上传。
                // url 都是 /upload，所以无法通过 url 实现取消功能，需要在具体方法中设置 signal
                if (config.signal) return config;

                const controller = new AbortController();
                const url = config.url || "";
                config.signal = controller.signal;
                this.abortControllerMap.set(url, controller);
                return config;
            },
            (err: AxiosError) => {
                throw err;
            }
        );
        // 响应完，从map中去除 url
        this.instance.interceptors.response.use(
            res => {
                const url = res?.config.url || "";
                this.abortControllerMap.delete(url);
                return res;
            },
            (err: AxiosError) => {
                const url = err?.config?.url || "";
                this.abortControllerMap.delete(url);
                throw err;
            }
        );

        /* * 实例级别拦截（实例化 axios 实例时可以添加的拦截器）*/
        if (config.interceptors) {
            this.instance.interceptors.request.use(
                config.interceptors.reqInterceptorOnFulfilled,
                config.interceptors.reqInterceptorOnRejected
            );
            this.instance.interceptors.response.use(
                config.interceptors.resInterceptorOnFulfilled,
                config.interceptors.resInterceptorOnRejected
            );
        }
    }

    /**
     * Sends a request and returns a Promise that resolves with the response data.
     * @template TReqBodyData - The type of the request body.
     * @template TResData - The type of the `data` field in the `{code, data}` response structure.
     * @template TResStructure - The type of the response structure. The default is `{code, data, msg}`.
     * @param {MyAxiosRequestConfig} [config] - The custom configuration for the request.
     * @returns {Promise<TResStructure>} - A Promise that resolves with the response data.
     * @throws {Error} - If the request fails.
     *
     * @example
     * // Sends a GET request and expects a response with a JSON object.
     * const response = await request<any, {name: string}>({
     *   method: 'GET',
     *   url: '/users/1',
     * });
     * console.log(response.name); // logs the name of the user
     */
    request<TReqBodyData, TResData, TResType = ResStructure<TResData>>(
        config: MyAxiosRequestConfig<TReqBodyData>
    ): Promise<TResType> {
        return new Promise<TResType>((resolve, reject) => {
            /* * 请求级别拦截（作用于具体某个请求的拦截器）*/
            // 手动执行请求拦截器
            if (config.interceptors?.reqInterceptorOnFulfilled) {
                config = config.interceptors.reqInterceptorOnFulfilled(config as InternalAxiosRequestConfig<TReqData>);
            }

            this.instance
                .request<any, AxiosResponse<TResType>>(config)
                .then(res => {
                    // 手动执行响应拦截器
                    if (config.interceptors?.resInterceptorOnFulfilled) {
                        res = config.interceptors.resInterceptorOnFulfilled(res);
                    }

                    resolve(res?.data);
                })
                .catch(err => {
                    if (config.interceptors?.resInterceptorOnRejected) {
                        err = config.interceptors.resInterceptorOnRejected(err);
                    }

                    reject(err);
                });
        });
    }

    /**
     * 发送 GET 请求
     * @template TReqBodyData 请求体数据类型
     * @template TResData 接口响应 data 字段数据类型
     * @template TResStructure 接口响应结构，默认为 {code, data, msg}
     * @param {MyAxiosRequestConfig} config 请求配置
     * @returns {Promise} 接口响应结果
     */
    get<TReqBodyData, TResData, TResType = ResStructure<TResData>>(
        config?: MyAxiosRequestConfig<TReqBodyData>
    ): Promise<TResType> {
        return this.request({ ...config, method: "GET" });
    }

    /**
     * 发送 post 请求
     * @template TReqBodyData 请求数据类型
     * @template TResData 响应 data 数据类型
     * @template TResType 响应结构，默认为返回类型的 {code, data, msg}
     * @param {MyAxiosRequestConfig} config 请求配置
     * @returns {Promise} 响应数据的 Promise
     */
    post<TReqBodyData, TResData, TResType = ResStructure<TResData>>(
        config: MyAxiosRequestConfig<TReqBodyData>
    ): Promise<TResType> {
        return this.request({ ...config, method: "POST" });
    }

    patch<TReqBodyData, TResData, TResType = ResStructure<TResData>>(
        config: MyAxiosRequestConfig<TReqBodyData>
    ): Promise<TResType> {
        return this.request({ ...config, method: "PATCH" });
    }

    delete<TReqBodyData, TResData, TResType = ResStructure<TResData>>(
        config?: MyAxiosRequestConfig<TReqBodyData>
    ): Promise<TResType> {
        return this.request({ ...config, method: "DELETE" });
    }

    /**
     * 获取当前 axios 实例
     */
    getInstance(): AxiosInstance {
        return this.instance;
    }

    /**
     * 取消全部请求
     */
    cancelAllRequest() {
        for (const [, controller] of this.abortControllerMap) {
            controller.abort();
        }
        this.abortControllerMap.clear();
    }

    /**
     * 取消指定的请求
     * （并发上传文件的url通常是一样的，通过url取消请求会取消所有上传操作，故此方法不宜用来取消上传请求）
     * @param url 待取消的请求URL
     */
    cancelRequest(url: string | string[]) {
        const urlList = Array.isArray(url) ? url : [url];
        urlList.forEach(_url => {
            this.abortControllerMap.get(_url)?.abort();
            this.abortControllerMap.delete(_url);
        });
    }

    /**
     * 单个文件上传
     * @param {AxiosRequestConfig} config axios 请求配置对象
     * @param {UploadFileParams} params 待上传文件及其一些参数
     * @param {(event: AxiosProgressEvent) => void} uploadProgress 上传进度的回调函数
     * @param {AbortSignal}cancelSignal 取消axios请求的 signal
     * @returns
     */
    uploadFile<TOtherFormData>(
        config: AxiosRequestConfig,
        params: UploadFileParams<TOtherFormData>,
        uploadProgress?: (event: AxiosProgressEvent) => void,
        cancelSignal?: AbortSignal
    ) {
        const formData = new window.FormData();

        // 设置默认文件表单字段为 file
        const customFilename = params.field || "file";

        // 是否指定文件名，没有就用文件本来的名字
        if (params.filename) {
            formData.append(customFilename, params.file, params.filename);
            // 另外如果调用时明确给出了文件名，则将文件名也作为表单数据之一发送
            formData.append("filename", params.filename);
        } else {
            formData.append(customFilename, params.file);
        }
        // 添加文件 hash
        if (params.fileHash) {
            formData.append("fileHash", params.fileHash);
        }

        // 是否有文件的额外信息补充进表单
        if (params.formData) {
            Object.keys(params.formData).forEach(key => {
                const value = params.formData![key as keyof TOtherFormData];
                if (Array.isArray(value)) {
                    value.forEach(item => {
                        // 对象属性值为数组时，表单字段加一个[]
                        formData.append(`${key}[]`, item);
                    });
                    return;
                }
                formData.append(key, value as any);
            });
        }

        return this.instance.request({
            ...config,
            method: "POST",
            timeout: 60 * 60 * 1000, // 60分钟，这里设置以避免被实例设置的超时时间影响
            data: formData,
            onUploadProgress: uploadProgress,
            signal: cancelSignal,
            headers: {
                "Content-type": "multipart/form-data;charset=UTF-8"
            }
        });
    }
}

export default HttpRequest;
